/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.

This file is part of Quake III Arena source code.

Quake III Arena source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.

Quake III Arena source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Quake III Arena source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/

#include "../qcommon/q_shared.h"
#include "../qcommon/qcommon.h"
#include "sys_local.h"

#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdio.h>
#include <dirent.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/time.h>
#include <pwd.h>
#include <libgen.h>
#include <fcntl.h>

qboolean        stdinIsATTY;

// Used to determine where to store user-specific files
static char     homePath[MAX_OSPATH] = { 0 };

/*
==================
Sys_DefaultHomePath
==================
*/
char           *Sys_DefaultHomePath(void)
{
	char           *p;

	if(!*homePath)
	{
		if((p = getenv("HOME")) != NULL)
		{
			Q_strncpyz(homePath, p, sizeof(homePath));
#ifdef MACOS_X
			Q_strcat(homePath, sizeof(homePath), "/Library/Application Support/" PRODUCT_NAME);
#else
			Q_strcat(homePath, sizeof(homePath), "/." PRODUCT_NAME_LOWER);
#endif
			if(mkdir(homePath, 0777))
			{
				if(errno != EEXIST)
				{
					Sys_Error("Unable to create directory \"%s\", error is %s(%d)\n", homePath, strerror(errno), errno);
				}
			}
		}
	}

	return homePath;
}

#ifndef MACOS_X
/*
================
Sys_TempPath
================
*/
const char     *Sys_TempPath(void)
{
	const char     *TMPDIR = getenv("TMPDIR");

	if(TMPDIR == NULL || TMPDIR[0] == '\0')
		return "/tmp";
	else
		return TMPDIR;
}
#endif

/*
================
Sys_Milliseconds
================
*/
/* base time in seconds, that's our origin
   timeval:tv_sec is an int:
   assuming this wraps every 0x7fffffff - ~68 years since the Epoch (1970) - we're safe till 2038
   using unsigned long data type to work right with Sys_XTimeToSysTime */
unsigned long   sys_timeBase = 0;

/* current time in ms, using sys_timeBase as origin
   NOTE: sys_timeBase*1000 + curtime -> ms since the Epoch
     0x7fffffff ms - ~24 days
   although timeval:tv_usec is an int, I'm not sure wether it is actually used as an unsigned int
     (which would affect the wrap period) */
int             curtime;
int Sys_Milliseconds(void)
{
	struct timeval  tp;

	gettimeofday(&tp, NULL);

	if(!sys_timeBase)
	{
		sys_timeBase = tp.tv_sec;
		return tp.tv_usec / 1000;
	}

	curtime = (tp.tv_sec - sys_timeBase) * 1000 + tp.tv_usec / 1000;

	return curtime;
}


/*
==================
Sys_RandomBytes
==================
*/
qboolean Sys_RandomBytes(byte * string, int len)
{
	FILE           *fp;

	fp = fopen("/dev/urandom", "r");
	if(!fp)
		return qfalse;

	if(!fread(string, sizeof(byte), len, fp))
	{
		fclose(fp);
		return qfalse;
	}

	fclose(fp);
	return qtrue;
}

/*
==================
Sys_GetCurrentUser
==================
*/
char           *Sys_GetCurrentUser(void)
{
	struct passwd  *p;

	if((p = getpwuid(getuid())) == NULL)
	{
		return "player";
	}
	return p->pw_name;
}

/*
==================
Sys_GetClipboardData
==================
*/
char           *Sys_GetClipboardData(void)
{
	return NULL;
}

#define MEM_THRESHOLD 96*1024*1024

/*
==================
Sys_LowPhysicalMemory

TODO
==================
*/
qboolean Sys_LowPhysicalMemory(void)
{
	return qfalse;
}

/*
==================
Sys_Basename
==================
*/
const char     *Sys_Basename(char *path)
{
	return basename(path);
}

/*
==================
Sys_Dirname
==================
*/
const char     *Sys_Dirname(char *path)
{
	return dirname(path);
}

/*
==================
Sys_Mkdir
==================
*/
qboolean Sys_Mkdir(const char *path)
{
	int             result = mkdir(path, 0750);

	if(result != 0)
		return errno == EEXIST;

	return qtrue;
}

/*
==================
Sys_Cwd
==================
*/
char           *Sys_Cwd(void)
{
	static char     cwd[MAX_OSPATH];

	char           *result = getcwd(cwd, sizeof(cwd) - 1);

	if(result != cwd)
		return NULL;

	cwd[MAX_OSPATH - 1] = 0;

	return cwd;
}

/*
==============================================================

DIRECTORY SCANNING

==============================================================
*/

#define MAX_FOUND_FILES 0x1000

/*
==================
Sys_ListFilteredFiles
==================
*/
void Sys_ListFilteredFiles(const char *basedir, char *subdirs, char *filter, char **list, int *numfiles)
{
	char            search[MAX_OSPATH], newsubdirs[MAX_OSPATH];
	char            filename[MAX_OSPATH];
	DIR            *fdir;
	struct dirent  *d;
	struct stat     st;

	if(*numfiles >= MAX_FOUND_FILES - 1)
	{
		return;
	}

	if(strlen(subdirs))
	{
		Com_sprintf(search, sizeof(search), "%s/%s", basedir, subdirs);
	}
	else
	{
		Com_sprintf(search, sizeof(search), "%s", basedir);
	}

	if((fdir = opendir(search)) == NULL)
	{
		return;
	}

	while((d = readdir(fdir)) != NULL)
	{
		Com_sprintf(filename, sizeof(filename), "%s/%s", search, d->d_name);
		if(stat(filename, &st) == -1)
			continue;

		if(st.st_mode & S_IFDIR)
		{
			if(Q_stricmp(d->d_name, ".") && Q_stricmp(d->d_name, ".."))
			{
				if(strlen(subdirs))
				{
					Com_sprintf(newsubdirs, sizeof(newsubdirs), "%s/%s", subdirs, d->d_name);
				}
				else
				{
					Com_sprintf(newsubdirs, sizeof(newsubdirs), "%s", d->d_name);
				}
				Sys_ListFilteredFiles(basedir, newsubdirs, filter, list, numfiles);
			}
		}
		if(*numfiles >= MAX_FOUND_FILES - 1)
		{
			break;
		}
		Com_sprintf(filename, sizeof(filename), "%s/%s", subdirs, d->d_name);
		if(!Com_FilterPath(filter, filename, qfalse))
			continue;
		list[*numfiles] = CopyString(filename);
		(*numfiles)++;
	}

	closedir(fdir);
}

/*
==================
Sys_ListFiles
==================
*/
char          **Sys_ListFiles(const char *directory, const char *extension, char *filter, int *numfiles, qboolean wantsubs)
{
	struct dirent  *d;
	DIR            *fdir;
	qboolean        dironly = wantsubs;
	char            search[MAX_OSPATH];
	int             nfiles;
	char          **listCopy;
	char           *list[MAX_FOUND_FILES];
	int             i;
	struct stat     st;

	int             extLen;

	if(filter)
	{

		nfiles = 0;
		Sys_ListFilteredFiles(directory, "", filter, list, &nfiles);

		list[nfiles] = NULL;
		*numfiles = nfiles;

		if(!nfiles)
			return NULL;

		listCopy = Z_Malloc((nfiles + 1) * sizeof(*listCopy));
		for(i = 0; i < nfiles; i++)
		{
			listCopy[i] = list[i];
		}
		listCopy[i] = NULL;

		return listCopy;
	}

	if(!extension)
		extension = "";

	if(extension[0] == '/' && extension[1] == 0)
	{
		extension = "";
		dironly = qtrue;
	}

	extLen = strlen(extension);

	// search
	nfiles = 0;

	if((fdir = opendir(directory)) == NULL)
	{
		*numfiles = 0;
		return NULL;
	}

	while((d = readdir(fdir)) != NULL)
	{
		Com_sprintf(search, sizeof(search), "%s/%s", directory, d->d_name);
		if(stat(search, &st) == -1)
			continue;
		if((dironly && !(st.st_mode & S_IFDIR)) || (!dironly && (st.st_mode & S_IFDIR)))
			continue;

		if(*extension)
		{
			if(strlen(d->d_name) < strlen(extension) || Q_stricmp(d->d_name + strlen(d->d_name) - strlen(extension), extension))
			{
				continue;		// didn't match
			}
		}

		if(nfiles == MAX_FOUND_FILES - 1)
			break;
		list[nfiles] = CopyString(d->d_name);
		nfiles++;
	}

	list[nfiles] = NULL;

	closedir(fdir);

	// return a copy of the list
	*numfiles = nfiles;

	if(!nfiles)
	{
		return NULL;
	}

	listCopy = Z_Malloc((nfiles + 1) * sizeof(*listCopy));
	for(i = 0; i < nfiles; i++)
	{
		listCopy[i] = list[i];
	}
	listCopy[i] = NULL;

	return listCopy;
}

/*
==================
Sys_FreeFileList
==================
*/
void Sys_FreeFileList(char **list)
{
	int             i;

	if(!list)
	{
		return;
	}

	for(i = 0; list[i]; i++)
	{
		Z_Free(list[i]);
	}

	Z_Free(list);
}

/*
==================
Sys_Sleep

Block execution for msec or until input is recieved.
==================
*/
void Sys_Sleep(int msec)
{
	if(msec == 0)
		return;

	if(stdinIsATTY)
	{
		fd_set          fdset;

		FD_ZERO(&fdset);
		FD_SET(STDIN_FILENO, &fdset);
		if(msec < 0)
		{
			select(STDIN_FILENO + 1, &fdset, NULL, NULL, NULL);
		}
		else
		{
			struct timeval  timeout;

			timeout.tv_sec = msec / 1000;
			timeout.tv_usec = (msec % 1000) * 1000;
			select(STDIN_FILENO + 1, &fdset, NULL, NULL, &timeout);
		}
	}
	else
	{
		// With nothing to select() on, we can't wait indefinitely
		if(msec < 0)
			msec = 10;

		usleep(msec * 1000);
	}
}

/*
==============
Sys_ErrorDialog

Display an error message
==============
*/
void Sys_ErrorDialog(const char *error)
{
	char            buffer[1024];
	unsigned int    size;
	int             f = -1;
	const char     *homepath = Cvar_VariableString("fs_homepath");
	const char     *gamedir = Cvar_VariableString("fs_gamedir");
	const char     *fileName = "crashlog.txt";
	char           *ospath = FS_BuildOSPath(homepath, gamedir, fileName);

	Sys_Print(va("%s\n", error));

#ifndef DEDICATED
	Sys_Dialog(DT_ERROR, va("%s. See \"%s\" for details.", error, ospath), "Error");
#endif

	// Make sure the write path for the crashlog exists...
	if(FS_CreatePath(ospath))
	{
		Com_Printf("ERROR: couldn't create path '%s' for crash log.\n", ospath);
		return;
	}

	// We might be crashing because we maxed out the Quake MAX_FILE_HANDLES,
	// which will come through here, so we don't want to recurse forever by
	// calling FS_FOpenFileWrite()...use the Unix system APIs instead.
	f = open(ospath, O_CREAT | O_TRUNC | O_WRONLY, 0640);
	if(f == -1)
	{
		Com_Printf("ERROR: couldn't open %s\n", fileName);
		return;
	}

	// We're crashing, so we don't care much if write() or close() fails.
	while((size = CON_LogRead(buffer, sizeof(buffer))) > 0)
	{
		if(write(f, buffer, size) != size)
		{
			Com_Printf("ERROR: couldn't fully write to %s\n", fileName);
			break;
		}
	}

	close(f);
}

#ifndef MACOS_X
/*
==============
Sys_ZenityCommand
==============
*/
static int Sys_ZenityCommand(dialogType_t type, const char *message, const char *title)
{
	const char     *options = "";
	char            command[1024];

	switch (type)
	{
		default:
		case DT_INFO:
			options = "--info";
			break;
		case DT_WARNING:
			options = "--warning";
			break;
		case DT_ERROR:
			options = "--error";
			break;
		case DT_YES_NO:
			options = "--question --ok-label=\"Yes\" --cancel-label=\"No\"";
			break;
		case DT_OK_CANCEL:
			options = "--question --ok-label=\"OK\" --cancel-label=\"Cancel\"";
			break;
	}

	Com_sprintf(command, sizeof(command), "zenity %s --text=\"%s\" --title=\"%s\"", options, message, title);

	return system(command);
}

/*
==============
Sys_KdialogCommand
==============
*/
static int Sys_KdialogCommand(dialogType_t type, const char *message, const char *title)
{
	const char     *options = "";
	char            command[1024];

	switch (type)
	{
		default:
		case DT_INFO:
			options = "--msgbox";
			break;
		case DT_WARNING:
			options = "--sorry";
			break;
		case DT_ERROR:
			options = "--error";
			break;
		case DT_YES_NO:
			options = "--warningyesno";
			break;
		case DT_OK_CANCEL:
			options = "--warningcontinuecancel";
			break;
	}

	Com_sprintf(command, sizeof(command), "kdialog %s \"%s\" --title \"%s\"", options, message, title);

	return system(command);
}

/*
==============
Sys_XmessageCommand
==============
*/
static int Sys_XmessageCommand(dialogType_t type, const char *message, const char *title)
{
	const char     *options = "";
	char            command[1024];

	switch (type)
	{
		default:
			options = "-buttons OK";
			break;
		case DT_YES_NO:
			options = "-buttons Yes:0,No:1";
			break;
		case DT_OK_CANCEL:
			options = "-buttons OK:0,Cancel:1";
			break;
	}

	Com_sprintf(command, sizeof(command), "xmessage -center %s \"%s\"", options, message);

	return system(command);
}

/*
==============
Sys_Dialog

Display a *nix dialog box
==============
*/
dialogResult_t Sys_Dialog(dialogType_t type, const char *message, const char *title)
{
	typedef enum
	{
		NONE = 0,
		ZENITY,
		KDIALOG,
		XMESSAGE,
		NUM_DIALOG_PROGRAMS
	} dialogCommandType_t;
	typedef int     (*dialogCommandBuilder_t) (dialogType_t, const char *, const char *);

	const char     *session = getenv("DESKTOP_SESSION");
	qboolean        tried[NUM_DIALOG_PROGRAMS] = { qfalse };
	dialogCommandBuilder_t commands[NUM_DIALOG_PROGRAMS] = { NULL };
	dialogCommandType_t preferredCommandType = NONE;

	commands[ZENITY] = &Sys_ZenityCommand;
	commands[KDIALOG] = &Sys_KdialogCommand;
	commands[XMESSAGE] = &Sys_XmessageCommand;

	// This may not be the best way
	if(!Q_stricmp(session, "gnome"))
		preferredCommandType = ZENITY;
	else if(!Q_stricmp(session, "kde"))
		preferredCommandType = KDIALOG;

	while(1)
	{
		int             i;
		int             exitCode;

		for(i = NONE + 1; i < NUM_DIALOG_PROGRAMS; i++)
		{
			if(preferredCommandType != NONE && preferredCommandType != i)
				continue;

			if(!tried[i])
			{
				exitCode = commands[i] (type, message, title);

				if(exitCode >= 0)
				{
					switch (type)
					{
						case DT_YES_NO:
							return exitCode ? DR_NO : DR_YES;
						case DT_OK_CANCEL:
							return exitCode ? DR_CANCEL : DR_OK;
						default:
							return DR_OK;
					}
				}

				tried[i] = qtrue;

				// The preference failed, so start again in order
				if(preferredCommandType != NONE)
				{
					preferredCommandType = NONE;
					break;
				}
			}
		}

		for(i = NONE + 1; i < NUM_DIALOG_PROGRAMS; i++)
		{
			if(!tried[i])
				continue;
		}

		break;
	}

	Com_DPrintf(S_COLOR_YELLOW "WARNING: failed to show a dialog\n");
	return DR_OK;
}
#endif

/*
==============
Sys_GLimpSafeInit

Unix specific "safe" GL implementation initialisation
==============
*/
void Sys_GLimpSafeInit(void)
{
	// NOP
}

/*
==============
Sys_GLimpInit

Unix specific GL implementation initialisation
==============
*/
void Sys_GLimpInit(void)
{
	// NOP
}

/*
==============
Sys_PlatformInit

Unix specific initialisation
==============
*/
void Sys_PlatformInit(void)
{
const char     *term = getenv("TERM");

stdinIsATTY = isatty(STDIN_FILENO) && !(term && (!strcmp(term, "raw") || !strcmp(term, "dumb")));

#if !defined(USE_JAVA)
	signal(SIGHUP, Sys_SigHandler);
	signal(SIGQUIT, Sys_SigHandler);
	signal(SIGTRAP, Sys_SigHandler);
	signal(SIGIOT, Sys_SigHandler);
	signal(SIGBUS, Sys_SigHandler);
#endif
}

/*
==============
Sys_SetEnv

set/unset environment variables (empty value removes it)
==============
*/

void Sys_SetEnv(const char *name, const char *value)
{
	if(value && *value)
		setenv(name, value, 1);
	else
		unsetenv(name);
}

/*
==============
Sys_PID
==============
*/
int Sys_PID(void)
{
	return getpid();
}

/*
==============
Sys_PIDIsRunning
==============
*/
qboolean Sys_PIDIsRunning(int pid)
{
	return kill(pid, 0) == 0;
}
